﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.ServiceModel;
using System.Threading;
using System.Configuration;
using System.Security;

namespace PSE.Hosting.ServiceHoster.Business
{
    public class ServiceHosterProcess : IDisposable
    {
        private System.Diagnostics.Process _process;
        private ProcessStartInfo _startInfo;
        private ManualResetEvent _processStarted = new ManualResetEvent(false);

        private string _packageName;

        private Timer _timer;
        private bool _hasError = false;

        public int Id
        {
            get
            {
                if (this.IsStarted)
                    return _process.Id;
                else
                    return -1;
            }
        }

        public string PackageName
        {
            get
            {
                return _packageName;
            }
        }

        private string _domain;
        /// <summary>
        /// 
        /// </summary>
        public string Domain
        {
            get { return _domain; }
            set { _domain = value; }
        }

        private string _userName;
        /// <summary>
        /// 
        /// </summary>
        public string UserName
        {
            get { return _userName; }
            set { _userName = value; }
        }

        private string _password;

        public string Password
        {
            get { return _password; }
            set { _password = value; }
        }

        #region IsStarted
        /// <summary>
        /// Indica se o processo foi iniciado
        /// </summary>
        public bool IsStarted
        {
            get
            {
                bool started = false;

                if (_process != null)
                {
                    _process.Refresh();
                    started = !_process.HasExited;
                }

                return started;
            }
        }
        #endregion

        #region Construtores
        /// <summary>
        /// Construtor padrão
        /// </summary>
        /// <param name="packageName">Nome do pacote</param>
        public ServiceHosterProcess(string packageName, string domain, string userName, string password)
        {
            if (string.IsNullOrEmpty(packageName))
                throw new ArgumentNullException("packageName");

            _packageName = packageName;
            _domain = domain;
            _userName = userName;
            _password = password;

            _startInfo = new ProcessStartInfo();

            _startInfo.FileName = "ServiceHosterProcess.exe";
            _startInfo.Arguments = "\"" + _packageName + "\"";
            _startInfo.CreateNoWindow = true;

            _startInfo.UseShellExecute = false;

            // Indica que o output e os erros do processo serão direcionados para o pai
            _startInfo.RedirectStandardInput = true;
            _startInfo.RedirectStandardOutput = true;
            _startInfo.RedirectStandardError = true;
        }
        #endregion

        #region Tratamento de eventos
        private void ServiceHosterProcess_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            // Ao receber dados do processo filho libera o método Start para continuar
            _processStarted.Set();
        }

        private void ServiceHosterProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            _hasError = true;

            // Ao receber um erro do processo filho libera o método Start para continuar
            _processStarted.Set();
        }
        #endregion

        #region StartProcess
        /// <summary>
        /// Inicia o processo
        /// </summary>
        /// <returns>True se iniciou o processo com sucesso, False se ocorreu algum erro</returns>
        public void StartProcess()
        {
            if (this.IsStarted)
                throw new InvalidOperationException("Cannot start process, it is already started");

            try
            {
                if (string.IsNullOrEmpty(_userName) || string.IsNullOrEmpty(_domain))
                {
                    _process = new System.Diagnostics.Process();
                    _process.StartInfo = _startInfo;

                    _process.OutputDataReceived += new DataReceivedEventHandler(ServiceHosterProcess_OutputDataReceived);
                    _process.ErrorDataReceived += new DataReceivedEventHandler(ServiceHosterProcess_ErrorDataReceived);

                    // Inicia o processo
                    _process.Start();

                    _process.BeginErrorReadLine();
                    _process.BeginOutputReadLine();

                    // Espera o inicio do processo
                    _processStarted.WaitOne();

                    if (_hasError)
                    {
                        throw new InvalidOperationException("Cannot start process, see log for details");
                    }

                    // Criando proxy do serviço do processo
                    using (ProcessService processService = new ProcessService(_packageName))
                    {
                        processService.GetInstance().IsAlive();
                    }
                }
                else
                {
                    // Utiliza API para iniciar o processo com outro usuário, System.Diagnostics.Process
                    // dispara Access Denied

                    _process = ProcessAsUser.Launch(_domain, _userName, _password, _startInfo.FileName + " " + _startInfo.Arguments);


                    //PSE.Hosting.ServiceHoster.Business.RunAs.ProcessInformation processInfo;
                    //PSE.Hosting.ServiceHoster.Business.RunAs.StartUpInfo startupInfo = new PSE.Hosting.ServiceHoster.Business.RunAs.StartUpInfo();

                    //startupInfo.cb = System.Runtime.InteropServices.Marshal.SizeOf(startupInfo);
                    //startupInfo.lpTitle = null;
                    //startupInfo.dwFlags = (int)PSE.Hosting.ServiceHoster.Business.RunAs.StartUpInfoFlags.UseShowWindow;
                    //startupInfo.wShowWindow = 0;
                    //startupInfo.lpDesktop = @"WinSta0\Default";

                    //_process = RunAs.StartProcess(_userName, _domain, _password,
                    //    PSE.Hosting.ServiceHoster.Business.RunAs.LogonFlags.WithProfile,
                    //    null,
                    //    _startInfo.FileName + " " + _startInfo.Arguments,
                    //    PSE.Hosting.ServiceHoster.Business.RunAs.CreationFlags.UnicodeEnvironment,
                    //    IntPtr.Zero,
                    //    null,
                    //    ref startupInfo,
                    //    out processInfo);

                    Exception lastEx = null;

                    // Espera 1 segundo pelo inicio do processo e tenta novamente por 5 vezes
                    for (int i = 0; i < 5; i++)
                    {
                        Thread.Sleep(1000);

                        try
                        {
                            // Criando proxy do serviço do processo
                            using (ProcessService processService = new ProcessService(_packageName))
                            {
                                processService.GetInstance().IsAlive();
                            }

                            _hasError = false;
                            break;
                        }
                        catch (Exception ex)
                        {
                            _hasError = true;
                            lastEx = ex;
                            continue;
                        }
                    }

                    if (_hasError)
                    {
                        throw new InvalidOperationException("Cannot start process, see log for details", lastEx);
                    }
                }

                // Capturando intervalo de verificação do processo filho
                int checkChildrenProcessInterval = Convert.ToInt32(ConfigurationManager.AppSettings["CheckChildrenProcessInterval"]);
                // Cria timer de verificação
                _timer = new Timer(new TimerCallback(CheckChildProcess), null, 0, checkChildrenProcessInterval);

            }
            catch
            {
                this.Dispose();
                throw;
            }

            return;
        }
        #endregion

        #region CloseProcess
        /// <summary>
        /// Fecha o processo
        /// </summary>
        public void CloseProcess()
        {
            if (!this.IsStarted)
                throw new InvalidOperationException("Cannot close process, it has not been started");

            this.Dispose();
        }
        #endregion

        #region CheckChildProcess
        /// <summary>
        /// Verifica se o processo filho está aberto e se estiver fechado o abre novamente
        /// </summary>
        /// <param name="state"></param>
        private void CheckChildProcess(object state)
        {
            if (!this.IsStarted)
            {
                this.Dispose();

                this.StartProcess();

                this.StartAll(true);
            }
        }
        #endregion

        #region VerifyIsStarted
        private void VerifyIsStarted()
        {
            if (!this.IsStarted)
                throw new InvalidOperationException("Cannot execute this operation, the process has not been started");
        }
        #endregion

        #region Proxy Methods
        
        /// <summary>
        /// Start all services a specified package.
        /// </summary>
        /// <param name="packageName">The package name to start the services.</param>
        public List<MessageData> StartAll(bool initialize)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                return processService.GetInstance().StartAll(initialize);
            }
        }

        /// <summary>
        /// Stop all the services from an assembly.
        /// </summary>
        /// <param name="packageName">The name of the package containing the assembly to be stopped.</param>
        /// <param name="codeBase">The name of the assembly to be stopped.</param>
        public void StopAssembly(string codeBase)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                processService.GetInstance().StopAssembly(codeBase);
            }
        }

        /// <summary>
        /// Start all WCF services from a specific assembly.
        /// </summary>
        /// <param name="packageName">The name of the package of the assembly to start.</param>
        /// <param name="fileName">The name of the assembly file to start. This is the filename only, without the path.</param>
        /// 
        public List<MessageData> StartAssembly(string fileName)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                return processService.GetInstance().StartAssembly(fileName);
            }
        }

        /// <summary>
        /// Starts a Service configured within the Hoster app.config.
        /// </summary>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to start.</param>
        public void StartService(string assembly, string serviceName)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                processService.GetInstance().StartService(assembly, serviceName);
            }
        }

        /// <summary>
        /// Stops the specified service.
        /// </summary>
        /// <param name="packageName">The name of the package containing the service.</param>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to stop.</param>
        public void StopService(string assembly, string serviceName)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                processService.GetInstance().StopService(assembly, serviceName);
            }
        }

        /// <summary>
        /// Stops all the services already started from a specific package.
        /// </summary>
        public void StopAll()
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                processService.GetInstance().StopAll();
            }
        }

        /// <summary>
        /// Gets a list of all the services configured within the Service Hoster app.config.
        /// </summary>
        /// <returns></returns>
        public List<Entities.Service> GetServiceList(string assembly)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                return processService.GetInstance().GetServiceList(assembly);
            }
        }

        /// <summary>
        /// Returns a list of all the assemblies added in a package.
        /// </summary>
        /// <param name="packageName">The name of the package to get the list of assenblies.</param>
        /// <returns>List of added assemblies.</returns>
        public List<Entities.Assembly> GetAssemblyList()
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                return processService.GetInstance().GetAssemblyList();
            }
        }

        /// <summary>
        /// Adds a Service to the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="codeBase">The assembly code base.</param>
        /// <param name="domain">The domain name of the user.</param>
        /// <param name="userName">The user name for impersonate service operations.</param>
        /// <param name="password">The user password.</param>
        public void AddAssembly(string codeBase)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                processService.GetInstance().AddAssembly(codeBase);
            }
        }

        /// <summary>
        /// Removes a Service from the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="assemblyFile">The name of the assembly file (without path).</param>
        public void RemoveAssembly(string assemblyFile)
        {
            this.VerifyIsStarted();

            using (ProcessService processService = new ProcessService(_packageName))
            {
                processService.GetInstance().RemoveAssembly(assemblyFile);
            }
        }
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            _hasError = false;
            _processStarted.Reset();

            // Finalizando timer
            if (_timer != null)
            {
                _timer.Change(Timeout.Infinite, Timeout.Infinite);
                _timer.Dispose();
                _timer = null;
            }


            // Finalizando canal
            try
            {
                if (this.IsStarted)
                {
                    using (ProcessService processService = new ProcessService(_packageName))
                    {
                        processService.GetInstance().CloseProcess();
                    }
                }
            }
            catch
            {
                try
                {
                    if (this.IsStarted)
                        _process.Kill();
                }
                catch
                {
                }
            }


            // Finalizando processo
            if (_process != null)
            {
                _process.OutputDataReceived -= new DataReceivedEventHandler(ServiceHosterProcess_OutputDataReceived);
                _process.ErrorDataReceived -= new DataReceivedEventHandler(ServiceHosterProcess_ErrorDataReceived);
                _process.Dispose();
                _process = null;
            }
        }

        #endregion
    }

    internal class ProcessService : IDisposable
    {
        private ChannelFactory<IServiceHosterProcessService> _processChannel;

        public ProcessService(string packageName)
        {
            EndpointAddress ep = new EndpointAddress(string.Format("net.pipe://localhost/ServiceHosterProcessService/{0}", packageName));
            NetNamedPipeBinding binding = new NetNamedPipeBinding();
            binding.OpenTimeout = new TimeSpan(1, 0, 0);
            binding.CloseTimeout = new TimeSpan(1, 0, 0);
            binding.SendTimeout = new TimeSpan(1, 0, 0);
            binding.ReceiveTimeout = new TimeSpan(1, 0, 0);

            binding.MaxBufferPoolSize = 128000000;
            binding.MaxBufferSize = 128000000;
            binding.MaxConnections = 1000;
            binding.MaxReceivedMessageSize = 128000000;

            _processChannel = new ChannelFactory<IServiceHosterProcessService>(binding, ep);
        }

        public IServiceHosterProcessService GetInstance()
        {
            return _processChannel.CreateChannel();
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (_processChannel != null)
            {
                // Finalizando canal
                try
                {
                    // Fecha o canal
                    if (_processChannel.State != CommunicationState.Faulted)
                        _processChannel.Close();
                }
                catch
                {
                    // Se ocorrer erro, aborta o canal
                    _processChannel.Abort();
                }

                _processChannel = null;
            }
        }

        #endregion
    }
}
